package healthcheck

import (
	"fmt"
	"time"

	"github.com/pingcap/errors"
	"github.com/romberli/das/config"
	"github.com/romberli/das/internal/app/metadata"
	"github.com/romberli/das/internal/dependency/healthcheck"
	"github.com/romberli/das/pkg/message"
	msghc "github.com/romberli/das/pkg/message/healthcheck"
	"github.com/romberli/das/pkg/util/deployment"
	utilhc "github.com/romberli/das/pkg/util/healthcheck"
	"github.com/romberli/go-util/common"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/go-util/middleware/clickhouse"
	"github.com/romberli/go-util/middleware/mysql"
	"github.com/romberli/go-util/middleware/prometheus"
	"github.com/romberli/log"
	"github.com/spf13/viper"
)

const (
	defaultOperationHistoriesNum = 100

	healthcheckResultStruct        = "Result"
	defaultMonitorClickhouseDBName = "pmm"
	defaultMonitorMySQLDBName      = "pmm"
	defaultRunningStatus           = 1
	defaultSuccessStatus           = 2
	defaultFailedStatus            = 3
)

var _ healthcheck.Service = (*Service)(nil)

// Service of health check
type Service struct {
	healthcheck.DASRepo
	OperationInfo      healthcheck.OperationInfo
	Engine             healthcheck.Engine
	Result             healthcheck.Result             `json:"result"`
	OperationHistories []healthcheck.OperationHistory `json:"operation_histories"`
}

// NewService returns a new *Service
func NewService(repo healthcheck.DASRepo) healthcheck.Service {
	return newService(repo)
}

// NewServiceWithDefault returns a new healthcheck.Service with default repository
func NewServiceWithDefault() healthcheck.Service {
	return newService(NewDASRepoWithGlobal())

}

// newService returns a new *Service
func newService(repo healthcheck.DASRepo) *Service {
	return &Service{
		DASRepo: repo,
		Result:  NewEmptyResult(),
	}
}

// GetDASRepo returns the das repository
func (s *Service) GetDASRepo() healthcheck.DASRepo {
	return s.DASRepo
}

// GetOperationInfo returns the operation information
func (s *Service) GetOperationInfo() healthcheck.OperationInfo {
	return s.OperationInfo
}

// GetEngine returns the healthcheck engine
func (s *Service) GetEngine() healthcheck.Engine {
	return s.Engine
}

// GetOperationHistories returns the operation histories
func (s *Service) GetOperationHistories() []healthcheck.OperationHistory {
	return s.OperationHistories
}

// GetResult returns the healthcheck result
func (s *Service) GetResult() healthcheck.Result {
	return s.Result
}

// GetOperationHistoryByID returns the operation history by operation id
func (s *Service) GetOperationHistoryByID(id int) error {
	oh, err := s.GetDASRepo().GetHealthCheckOperationHistoryByID(id)

	s.OperationHistories = nil
	s.OperationHistories = append(s.OperationHistories, oh)

	return err
}

// GetOperationHistoriesByLoginName returns the operation histories by login name
func (s *Service) GetOperationHistoriesByLoginName(loginName string) error {
	var err error

	s.OperationHistories, err = s.GetDASRepo().GetHealthCheckHistories(loginName, defaultOperationHistoriesNum)

	return err
}

// GetResultByOperationID gets the result of given operation id
func (s *Service) GetResultByOperationID(id int) error {
	var err error

	s.Result, err = s.GetDASRepo().GetResultByOperationID(id)

	return err
}

// CheckWithHTTP performs healthcheck on the mysql server with given mysql server id,
// initiating is synchronous, actual running is asynchronous
func (s *Service) CheckWithHTTP(mysqlServerID int, startTime, endTime time.Time, step time.Duration, loginName string) (int, error) {
	// init mysql server service
	mss := metadata.NewMySQLServerServiceWithDefault()
	// get mysql server
	err := mss.GetByID(mysqlServerID)
	if err != nil {
		return constant.ZeroInt, err
	}
	mysqlServer := mss.GetMySQLServers()[constant.ZeroInt]
	monitorSystem, err := mysqlServer.GetMonitorSystem()
	if err != nil {
		return constant.ZeroInt, err
	}

	return s.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(constant.ZeroInt,
		mysqlServer.GetHostIP(), mysqlServer.GetPortNum(), mysqlServer.GetServerName(), mysqlServer.GetServiceName(),
		monitorSystem.GetSystemType(), monitorSystem.GetHostIP(), monitorSystem.GetPortNum(), monitorSystem.GetPortNumSlow(), monitorSystem.GetBaseURL(),
		startTime, endTime, step, viper.GetString(config.ServerCallbackAddrKey), loginName)
}

// CheckByHostInfoWithHTTP performs healthcheck on the mysql server with given mysql server id,
// initiating is synchronous, actual running is asynchronous
func (s *Service) CheckByHostInfoWithHTTP(mysqlServerHostIP string, mysqlServerPortNum int,
	startTime, endTime time.Time, step time.Duration, loginName string) (int, error) {
	// init mysql server service
	mss := metadata.NewMySQLServerServiceWithDefault()
	// get mysql server
	err := mss.GetByHostInfo(mysqlServerHostIP, mysqlServerPortNum)
	if err != nil {
		return constant.ZeroInt, err
	}
	mysqlServer := mss.GetMySQLServers()[constant.ZeroInt]
	monitorSystem, err := mysqlServer.GetMonitorSystem()
	if err != nil {
		return constant.ZeroInt, err
	}

	return s.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(constant.ZeroInt,
		mysqlServer.GetHostIP(), mysqlServer.GetPortNum(), mysqlServer.GetServerName(), mysqlServer.GetServiceName(),
		monitorSystem.GetSystemType(), monitorSystem.GetHostIP(), monitorSystem.GetPortNum(), monitorSystem.GetPortNumSlow(), monitorSystem.GetBaseURL(),
		startTime, endTime, step, viper.GetString(config.ServerCallbackAddrKey), loginName)
}

// CheckByMySQLServerAndMonitorSystemInfoWithHTTP performs healthcheck on the mysql server with given mysql server info and monitor system info,
// initiating is synchronous, actual running is asynchronous,
// after checking, it will call the das server's api to save the result
func (s *Service) CheckByMySQLServerAndMonitorSystemInfoWithHTTP(operationID int,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServerName, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNum, monitorSystemPortNumSlow int, monitorSystemBaseURL string,
	startTime, endTime time.Time, step time.Duration, callbackAddr, loginName string) (int, error) {
	// check if operation with the same mysql server id is still running
	if !viper.GetBool(config.HealthCheckAllowConcurrentKey) {
		isRunning, opID, err := s.GetDASRepo().IsRunning(mysqlServerHostIP, mysqlServerPortNum)
		if err != nil {
			return operationID, err
		}
		if isRunning {
			return operationID, errors.Errorf(
				"healthcheck Service.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(): healthcheck of this mysql server is still running. operation id: %d, host ip: %s, port num: %d", opID, mysqlServerHostIP, mysqlServerPortNum)
		}
	}

	operationID, err := s.GetDASRepo().InitOperation(operationID, loginName, mysqlServerHostIP, mysqlServerPortNum, startTime, endTime, step)
	if err != nil {
		return operationID, err
	}

	if viper.GetString(config.RoleKey) == config.ServerRole {
		mysqlService := metadata.NewMySQLServerServiceWithDefault()
		err = mysqlService.GetByHostInfo(mysqlServerHostIP, mysqlServerPortNum)
		if err != nil {
			return operationID, err
		}
		mysqlServer := mysqlService.GetMySQLServers()[constant.ZeroInt]
		deploymentService := deployment.NewServiceWithDefault(mysqlServer)
		if !deploymentService.CheckAccessibility() {
			if callbackAddr == constant.EmptyString {
				return operationID, errors.Errorf(
					"healthcheck Service.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(): mysql server is not accessible, but callback server address is empty. host ip: %s, port num: %d", mysqlServerHostIP, mysqlServerPortNum)
			}

			data := []byte(fmt.Sprintf(utilhc.MySQLServerAndMonitorSystemInfoHTTPBodyTemplate, operationID,
				mysqlServerHostIP, mysqlServerPortNum, mysqlServerServerName, mysqlServerServiceName,
				monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNum, monitorSystemPortNumSlow, monitorSystemBaseURL,
				startTime.Format(constant.TimeLayoutSecond), endTime.Format(constant.TimeLayoutSecond), step.String(), callbackAddr, loginName))
			log.Debugf("healthcheck Service.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(): data: %s", string(data))
			// send request
			respBody, err := deploymentService.PostDAS(utilhc.CheckByMySQLServerAndMonitorSystemInfoPath, data)
			if err != nil {
				return operationID, err
			}
			log.Debugf("healthcheck Service.CheckByMySQLServerAndMonitorSystemInfoWithHTTP(): respBody: %s", respBody)
			return operationID, nil
		}
	}

	return operationID, s.CheckByMySQLServerAndMonitorSystemInfoLocal(operationID,
		mysqlServerHostIP, mysqlServerPortNum, mysqlServerServerName, mysqlServerServiceName,
		monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNum, monitorSystemPortNumSlow, monitorSystemBaseURL,
		startTime, endTime, step, callbackAddr, loginName)
}

// CheckByMySQLServerAndMonitorSystemInfoWithHTTP performs healthcheck on the mysql server with given mysql server info and monitor system info,
// initiating is synchronous, actual running is asynchronous,
// after checking, it will call the das server's api to save the result
func (s *Service) CheckByMySQLServerAndMonitorSystemInfoLocal(operationID int,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServerName, mysqlServerServiceName string,
	monitorSystemType int, monitorSystemHostIP string, monitorSystemPortNum, monitorSystemPortNumSlow int, monitorSystemBaseURL string,
	startTime, endTime time.Time, step time.Duration, callbackAddr, loginName string) error {
	// init engine
	err := s.initEngine(operationID, loginName,
		mysqlServerHostIP, mysqlServerPortNum, mysqlServerServerName, mysqlServerServiceName,
		monitorSystemType, monitorSystemHostIP, monitorSystemPortNum, monitorSystemPortNumSlow, monitorSystemBaseURL,
		startTime, endTime, step, callbackAddr)
	if err != nil {
		updateErr := s.GetDASRepo().UpdateOperationStatusLocal(operationID, defaultFailedStatus, err.Error())
		if updateErr != nil {
			log.Errorf(constant.LogWithStackString, message.NewMessage(msghc.ErrHealthcheckUpdateOperationStatus, updateErr, operationID))
		}

		return err
	}
	// run asynchronously
	go s.GetEngine().Run()

	return nil
}

// initEngine initializes the engine
func (s *Service) initEngine(operationID int, loginName string,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServerName, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNum, monitorSystemPortNumSlow int, monitorSystemBaseURL string,
	startTime, endTime time.Time, step time.Duration, callbackAddr string) error {
	// init operation information
	s.OperationInfo = NewOperationInfo(operationID, loginName,
		mysqlServerHostIP, mysqlServerPortNum, mysqlServerServerName, mysqlServerServiceName,
		monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNum, monitorSystemPortNumSlow, monitorSystemBaseURL,
		startTime, endTime, step, callbackAddr)
	// init application mysql connection
	mysqlServerAddr := fmt.Sprintf("%s:%d", mysqlServerHostIP, mysqlServerPortNum)
	applicationMySQLConn, err := mysql.NewConn(mysqlServerAddr, constant.EmptyString, s.getApplicationMySQLUser(), s.getApplicationMySQLPass())
	if err != nil {
		return message.NewMessage(
			msghc.ErrHealthcheckCreateApplicationMySQLConnection, err, mysqlServerAddr, s.getApplicationMySQLUser())
	}
	// init application mysql repository
	applicationMySQLRepo := NewApplicationMySQLRepo(s.GetOperationInfo(), applicationMySQLConn)

	var (
		prometheusConfig prometheus.Config
		queryRepo        healthcheck.QueryRepo
	)

	prometheusAddr := fmt.Sprintf("%s:%d%s", monitorSystemHostIP, monitorSystemPortNum, monitorSystemBaseURL)
	slowQueryAddr := fmt.Sprintf("%s:%d", monitorSystemHostIP, monitorSystemPortNumSlow)

	switch monitorSystemSystemType {
	case 1:
		// pmm 1.x
		// init prometheus config
		prometheusConfig = prometheus.NewConfig(prometheusAddr, prometheus.DefaultRoundTripper)
		// init mysql connection
		conn, err := mysql.NewConn(slowQueryAddr, defaultMonitorMySQLDBName, s.getMonitorMySQLUser(), s.getMonitorMySQLPass())
		if err != nil {
			return message.NewMessage(
				msghc.ErrHealthcheckCreateMonitorMySQLConnection, err, slowQueryAddr, s.getMonitorMySQLUser())
		}
		// init query repository
		queryRepo = NewMySQLQueryRepo(s.GetOperationInfo(), conn)
	case 2:
		// pmm 2.x
		// init prometheus config
		prometheusConfig = prometheus.NewConfigWithBasicAuth(prometheusAddr, s.getMonitorPrometheusUser(), s.getMonitorPrometheusPass())
		// init clickhouse connection
		conn, err := clickhouse.NewConnWithDefault(slowQueryAddr, defaultMonitorClickhouseDBName, s.getMonitorClickhouseUser(), s.getMonitorClickhousePass())
		if err != nil {
			return message.NewMessage(
				msghc.ErrHealthcheckCreateMonitorClickhouseConnection, err, slowQueryAddr, s.getMonitorClickhouseUser())
		}
		// init query repository
		queryRepo = NewClickhouseQueryRepo(s.GetOperationInfo(), conn)
	default:
		return errors.Errorf("healthcheck: monitor system type should be either 1 or 2, %d is not valid", monitorSystemSystemType)
	}

	// init prometheus connection
	prometheusConn, err := prometheus.NewConnWithConfig(prometheusConfig)
	if err != nil {
		return message.NewMessage(
			msghc.ErrHealthcheckCreateMonitorPrometheusConnection, err, prometheusAddr, s.getMonitorPrometheusUser())
	}
	// init prometheus repository
	prometheusRepo := NewPrometheusRepo(s.GetOperationInfo(), prometheusConn)
	// init engine
	s.Engine = NewDefaultEngine(s.GetOperationInfo(), s.GetDASRepo(), applicationMySQLRepo, prometheusRepo, queryRepo)

	return nil
}

// getApplicationMySQLUser returns application mysql username
func (s *Service) getApplicationMySQLUser() string {
	return viper.GetString(config.DBApplicationMySQLUserKey)
}

// getApplicationMySQLPass returns application mysql password
func (s *Service) getApplicationMySQLPass() string {
	return viper.GetString(config.DBApplicationMySQLPassKey)
}

// getMonitorPrometheusUser returns prometheus username of monitor system
func (s *Service) getMonitorPrometheusUser() string {
	return viper.GetString(config.DBMonitorPrometheusUserKey)
}

// getMonitorPrometheusPass returns prometheus password of monitor system
func (s *Service) getMonitorPrometheusPass() string {
	return viper.GetString(config.DBMonitorPrometheusPassKey)
}

// getMonitorClickhouseUser returns clickhouse username of monitor system
func (s *Service) getMonitorClickhouseUser() string {
	return viper.GetString(config.DBMonitorClickhouseUserKey)
}

// getMonitorClickhousePass returns clickhouse password of monitor system
func (s *Service) getMonitorClickhousePass() string {
	return viper.GetString(config.DBMonitorClickhousePassKey)
}

// getMonitorMySQLUser returns mysql username of monitor system
func (s *Service) getMonitorMySQLUser() string {
	return viper.GetString(config.DBMonitorMySQLUserKey)
}

// getMonitorMySQLPass returns mysql password of monitor system
func (s *Service) getMonitorMySQLPass() string {
	return viper.GetString(config.DBMonitorMySQLPassKey)
}

// ReviewAccuracy updates accuracy review with given operation id
func (s *Service) ReviewAccuracy(id, review int) error {
	return s.GetDASRepo().UpdateAccuracyReviewByOperationID(id, review)
}

// SaveResultLocal saves the result in the middleware
func (s *Service) SaveResultLocal(resultStr string) (int, error) {
	result := NewEmptyResult()
	err := result.Unmarshal([]byte(resultStr))
	if err != nil {
		return constant.ZeroInt, errors.Trace(err)
	}

	return result.GetOperationID(), s.GetDASRepo().SaveResultLocal(result)
}

// UpdateOperationStatusLocal updates the operation status
func (s *Service) UpdateOperationStatusLocal(operationID, status int, message string) error {
	return s.GetDASRepo().UpdateOperationStatusLocal(operationID, status, message)
}

// Marshal marshals Service to json bytes
func (s *Service) Marshal() ([]byte, error) {
	// return []byte(fmt.Sprintf(healthcheckMarshalServiceTemplate, s.GetResult().String())), nil
	return s.MarshalWithFields(healthcheckResultStruct)
}

// MarshalWithFields marshals only specified fields of the Service to json bytes
func (s *Service) MarshalWithFields(fields ...string) ([]byte, error) {
	return common.MarshalStructWithFields(s, fields...)
}
